package IC.Semantics.types;


import java.util.HashSet;
import java.util.LinkedHashMap;

import IC.AST.ICClass;
import IC.AST.Method;
import IC.AST.Type;
import IC.AST.UserType;
import IC.Semantics.SemanticError;

public class TypeTable 
{
	private static LinkedHashMap<String, SemanticType> primitiveTypes = new LinkedHashMap<String, SemanticType>();
	private static LinkedHashMap<String, ClassType> classTypes = new LinkedHashMap<String, ClassType>();
	private static LinkedHashMap<String, MethodType> methodTypes = new LinkedHashMap<String, MethodType>();
	private static LinkedHashMap<SemanticType, ArrayType> arrayTypes = new LinkedHashMap<SemanticType, ArrayType>();
	
	protected static int idGenerator = 0;
	
	public static void populatePrimitiveTypes()
	{
		primitiveTypes.put("int", new IntType());  
		primitiveTypes.put("boolean", new BooleanType());  
		primitiveTypes.put("null", new NullType());    
		primitiveTypes.put("string", new StringType()); 
		primitiveTypes.put("void", new VoidType()); 
	}
	
	/**
	 * adds a new ClassType entry to TypeTable. 
	 * @param classNode - the class we want to add to type table, as an AST-Node
	 * @throws SemanticError - If the class is already defined or extends a class that was not previously defined.
	 */  
	public static void addClassType(ICClass classNode) throws SemanticError
	{     
		// if this class already defined - semantic error
		if (classTypes.containsKey(classNode.getName()))         
			throw new SemanticError("class already defined", classNode.getLine(), classNode.getName());
		
		// if this class extends a class that was not yet defined - semantic error
		else if (classNode.hasSuperClass() && (!classTypes.containsKey(classNode.getSuperClassName())) )
			throw new SemanticError("super-class is undefined", classNode.getLine(), classNode.getSuperClassName());
		
		ClassType ct = new ClassType(classNode);   
		classTypes.put(classNode.getName(), ct);
	} 
	
		
	// ======================= type getters ======================
	
	/**
	 * 
	 * @param name - the name of the class we want to get the type for
	 * @return a unique class type for the given name. 
	 * @throws SemanticError - if trying to get a class type that was not previously defined
	 */
	public static ClassType getClassType(String name)  
	{    
		return classTypes.get(name);
	}
	
	/**
	 * 
	 * @param elementType
	 * @return the arrayTye object for the given element, and add it if it doesn't already exist in type table
	 */
	public static ArrayType getArrayType(SemanticType elementType)
	{
		if (!arrayTypes.containsKey(elementType)) 
			arrayTypes.put(elementType, new ArrayType(elementType));

		return arrayTypes.get(elementType);
	}

	/**
	 * return method type and add it to types if not exist
	 * @param m - a method AST node
	 * @param className 
	 * @return - the unique method type of the given method. 
	 * @throws SemanticError - if a method with the same name but with a different signature is a already defined
	 */
	public static MethodType getMethodType(Method m, String className) throws SemanticError 
	{
		MethodType mt = new MethodType(m, className);
		
		// using the method type name and not the method to include the method class name in the key name
		if (!methodTypes.containsKey(className + mt.name))
			methodTypes.put(className + mt.name, mt);
		
		else
		{
			MethodType mt2 = methodTypes.get(className + mt.name);
			
			// if not equals then we are trying to define a new method with same name but different signature which is illegal in IC
			// here we use the overridden equals method of the MethodType class
			if (!mt.equals(mt2))
				throw new SemanticError(SemanticError.METHOD_ALREADY_DEFINED, m.getLine(), mt.name);
		}
			
		return mt;
	}
	
	public static SemanticType getPrimitiveType(String name) {
		return primitiveTypes.get(name);
	}
	

	/**
	 * 
	 * @param t - an AST node type describing a primitive, an array or a user defined type (i.e a class)
	 * @return - the corresponding semantic type from the type table 
	 * @throws SemanticError - in case the type is a class type that was not previously defined
	 */
	public static SemanticType getType(Type t)
	{
		// if array type
		int d = t.getDimension();
		if (d > 0)
		{
			// create a new type of a dimension 1 size lower
			Type t2 = new UserType(t.getLine(), t.getName());
			
			for (int i = 0; i < d-1; i++)
				t2.incrementDimension();
			
			return getArrayType(getType(t2));
		}
				
		// if primitive type
		if (primitiveTypes.containsKey(t.getName()))
			return primitiveTypes.get(t.getName());
		
		else
			return getClassType(t.getName());	
	}
	
	/**
     * returns string representation for the TypeTable fitting the "-dump-symtab" IC.Compiler flag
     * @return
     */
    public static String dump(String ICFilePath)
    {
        StringBuilder typeTableString = new StringBuilder("Type Table: "+ICFilePath+"\n");
        
        // Iterate over the primitive types in program type-table
        for (SemanticType pt: primitiveTypes.values()){
        	typeTableString.append("\t"+pt.getTypeTableID()+": Primitive type: "+pt.toString()+"\n");
        }
        // Iterate over the class types in program type-table
        for (SemanticType ct: classTypes.values()){
        	typeTableString.append("\t"+ct.getTypeTableID()+": Class: "+ct.toString()+"\n");
        } 
        // Iterate over the array types in program type-table
        for (SemanticType at: arrayTypes.values()){
        	typeTableString.append("\t"+at.getTypeTableID()+": Array type: "+at.toString()+"\n");
        }
        // Iterate over the method types in program type-table
        // because we save the methods with their names we need to filter only by their signature
        HashSet<String> signatures = new HashSet<String>(methodTypes.size());
        for (SemanticType mt: methodTypes.values())
        {
        	String str = mt.toString();
        	if (signatures.contains(str))
        		continue;
        	
        	signatures.add(str);
        	typeTableString.append("\t"+mt.getTypeTableID()+": Method type: "+ str +"\n");
        }
        return typeTableString.toString();
    }
}
